package org.gap.jseed;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import org.gap.jseed.injection.ClassWriter;
import org.gap.jseed.injection.annotation.SingletonBuilder;
import org.gap.jseed.utils.Defense;

/**
 * Used to bind a key to a service.
 */
public final class ServiceStoreContainer implements SeedContainer {
	private Map<Class<?>, Builder<?>> createdInstances;
	private List<BuilderFactory> builderFactories;
	private List<Injector> injectorFactories;
	private Map<Class<?>, Object> services;

	public ServiceStoreContainer() {
		createdInstances = new HashMap<Class<?>, Builder<?>>();
		builderFactories = new ArrayList<BuilderFactory>();
		injectorFactories = new ArrayList<Injector>();
		services = new HashMap<Class<?>, Object>();
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see org.gap.jseed.SeedContainer2#hasService(java.lang.Class)
	 */
	@Override
	public boolean hasService(Class<?> key) {
		if (key.equals(SeedContainer.class)) {
			return true;
		}
		return services.containsKey(key);
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see org.gap.jseed.SeedContainer2#bind(java.lang.Class,
	 * org.gap.jseed.Builder)
	 */
	@Override
	public <T> void bind(Class<T> key, Builder<? extends T> builder) {
		validateBinding(key, builder.getType());
		validateBuilder(key, builder);

		BuilderChainFactory<T> factory = new BuilderChainFactory<T>(builder);
		builder = factory.createChain();

		services.put(key, builder);
	}
	
	/*
	 * (non-Javadoc)
	 * 
	 * @see org.gap.jseed.SeedContainer2#bind(java.lang.Class, java.lang.Object)
	 */
	@Override
	@SuppressWarnings({ "unchecked", "rawtypes" })
	public void bind(Class<?> key, Object service) {
		if (service instanceof Builder) {
			bind(key, (Builder)service);
		}
		validateBinding(key, service);
		
		services.put(key, service);
	}

	private void validateBinding(Class<?> key, Object service) {
		Defense.argumentNotNull(key, "Key to be bound cannot be null");
		Defense.argumentNotNull(service, "%s cannot be bound to a null service", key.getName());
		Defense.keyNotInMap(key, services, "%s has already been bound", key.getName());
	}

	private <T> void validateBuilder(Class<T> key, Builder<? extends T> serviceBuilder) {
		if (!key.isAssignableFrom(serviceBuilder.getType())) {
			String message = String.format("The bound service %s does not match the key type %s", serviceBuilder.getType().getName(), key.getName());
			throw new IllegalArgumentException(message);
		}
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see org.gap.jseed.SeedContainer2#bind(java.lang.Class, java.lang.Class)
	 */
	@Override
	public <T> void bind(Class<T> key, Class<? extends T> service) {
		validateBinding(key, service);
		bind(key, createBuilder(service));
	}

	@Override
	public <T> void register(Class<T> service) {
		bind(service, service);
	}
	
	/*
	 * (non-Javadoc)
	 * 
	 * @see org.gap.jseed.SeedContainer2#get(java.lang.Class)
	 */
	@Override
	@SuppressWarnings({ "unchecked", "rawtypes" })
	public <T> T get(Class<T> key) {
		Defense.argumentNotNull(key, "Services cannot be registered with null key");

		if (SeedContainer.class.isAssignableFrom(key)) {
			return (T)this;
		}
		if (hasService(key)) {
			Object service = services.get(key);
			if (service instanceof Builder) {
				service = (((Builder) service).createInstance());
			}
			return (T) service;
		}
		throw new IllegalStateException(String.format("'%s' has not been registered", key.getName()));
	}

	/**
	 * Number of services currently bound.
	 * 
	 * @return
	 */
	public Object count() {
		return services.size();
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see org.gap.jseed.SeedContainer2#create(java.lang.Class)
	 */
	@SuppressWarnings("unchecked")
	@Override
	public <T> T create(Class<T> toCreate) {
		if (services.containsKey(toCreate)) {
			return get(toCreate);
		} else if (createdInstances.containsKey(toCreate)) {
			return (T)createdInstances.get(toCreate).createInstance();
		} else {
			Builder<T> builder = createBuilder(toCreate);
			createdInstances.put(toCreate, builder);
			return builder.createInstance();
		}
	}

	private <T> Builder<T> createBuilder(Class<? extends T> service) {
		Class<? extends T> reWritten = injectCode(service);
		Builder<T> builder = makeBuilderChain(service, reWritten);
		return builder;
	}

	private <T> Class<? extends T> injectCode(Class<? extends T> service) {
		ClassWriter writer = new ClassWriter(service);
		for (Injector each : injectorFactories) {
			each.inject(writer, service);
		}
		return writer.toJavaClass();
	}

	private <T> Builder<T> makeBuilderChain(Class<? extends T> service, Class<? extends T> reWritten) {
		Builder<T> builder = new DependencyInjectedBuilder<T>(reWritten, this);
		for (BuilderFactory each : builderFactories) {
			builder = each.<T> createBuilder(builder);
		}
		builder = new SingletonBuilder<T>(builder, service);
		return builder;
	}

	public void registerBuilderFactory(BuilderFactory builderFactory) {
		builderFactories.add(builderFactory);
	}

	public void registerInjector(Injector injectorFactory) {
		injectorFactories.add(injectorFactory);
	}
}
